package org.datacoder.geocoding

import com.vividsolutions.jts.geom.Coordinate
import com.vividsolutions.jts.geom.Envelope
import com.vividsolutions.jts.geom.GeometryFactory
import com.vividsolutions.jts.geom.Point
import org.datacoder.geocoding.jurisdiction.*
import org.datacoder.util.GeneralUtils
import org.datacoder.util.ParameterException

class QueryController {

    private static final Map<String, Class> nameJurisMap = initializeNameJurisMap()
    private GeometryFactory geometryFactory = GeocodingUtils.geometryFactory
    GeocodingService geocodingService
    Map<String, String> defaultParamsMap = [
        query           :null,
        name            :null,
        jurisdiction    :null,
        jurisdictions   :null,
        point           :null,
        polygon         :null,
        miles           :"10.0",
        type            :null,
        parent          :null,
        numResults      :"100"
    ]
    Map<String, String> queryParamsMap = [
        /* NON-GEOCODING QUERIES */
        findById                                    :["jurisdiction"],
        findByName                                  :["name"],
        findChildJurisdictions                      :["jurisdiction"],
        findCentroid                                :["jurisdiction"],
        findContainingRectangle                     :["jurisdictions"],
        /* GEOCODING QUERIES */
        findJurisdictionsNearbyPoint                :["point", "miles"],
        findJurisdictionsContainingJurisdiction     :["jurisdiction"],
        findJurisdictionsWithinJurisdiction         :["jurisdiction"],
        findJurisdictionsIntersectingJurisdiction   :["jurisdiction"],
        findJurisdictionsNearbyJurisdiction         :["jurisdiction", "miles"],
        findJurisdictionsContainingGeometry         :["polygon"],
        findJurisdictionsWithinGeometry             :["polygon"],
        findJurisdictionsIntersectingGeometry       :["polygon"],
        findJurisdictionsNearbyGeometry             :["polygon", "miles"]
    ]
    Map<String, Closure> queryMethodMap = [
        /* NON-GEOCODING QUERIES */
        findById                                    :{return params["jurisdiction"]},
        findByName                                  :{return geocodingService.findByName(params["name"])},
        findChildJurisdictions                      :{return geocodingService.findChildJurisdictions(params["jurisdiction"])},
        findCentroid                                :{return geocodingService.findCentroid(params["jurisdiction"])},
        findContainingRectangle                     :{return geocodingService.findContainingRectangle(params["jurisdictions"])},
        /* GEOCODING QUERIES */
        findJurisdictionsNearbyPoint                :{return geocodingService.findJurisdictionsNearbyPoint(params["point"], params["miles"])},
        findJurisdictionsContainingJurisdiction     :{return geocodingService.findJurisdictionsContainingJurisdiction(params["jurisdiction"])},
        findJurisdictionsWithinJurisdiction         :{return geocodingService.findJurisdictionsWithinJurisdiction(params["jurisdiction"])},
        findJurisdictionsIntersectingJurisdiction   :{return geocodingService.findJurisdictionsIntersectingJurisdiction(params["jurisdiction"])},
        findJurisdictionsNearbyJurisdiction         :{return geocodingService.findJurisdictionsNearbyJurisdiction(params["jurisdiction"], params["miles"])},
        findJurisdictionsContainingGeometry         :{return geocodingService.findJurisdictionsContainingGeometry(params["polygon"])},
        findJurisdictionsWithinGeometry             :{return geocodingService.findJurisdictionsWithinGeometry(params["polygon"])},
        findJurisdictionsIntersectingGeometry       :{return geocodingService.findJurisdictionsIntersectingGeometry(params["polygon"])},
        findJurisdictionsNearbyGeometry             :{return geocodingService.findJurisdictionsNearbyGeometry(params["polygon"], params["miles"])}
    ]
    
    /*
     * PARAMS
     * (Necessary)
     * query = CRASH (Method)
     * name = null (String)
     * jurisdiction = null (Long)
     * jurisdictions = null (Long...)
     * point = null ((Double, Double))
     * polygon = null ((Double, Double)...)
     * miles = 10.0 (Double)
     * 
     * (Optional)
     * type = null (Class.getSimpleName())
     * parent = null (Long)
     * numResults = 100 (Integer)
     */
    
    /*
     * RESULTS
     * Set<Jurisdiction> (id, name, longName, parentId)
     * Jurisdiction (id, name, longName, parentId)
     * Point ((Double, Double))
     * Envelope ((Double, Double)...)
     */

    private void preprocessParams(){
        // Set all null parameters to defaults
        for(defaultParam in defaultParamsMap.keySet()){
            if(params[defaultParam] == null){
                params[defaultParam] = defaultParamsMap[defaultParam]
            }
        }
        String queryString = params["query"]
        // Do we have a valid query?
        if(!queryParamsMap.containsKey(queryString)){
            throw new ParameterException("Query \"${queryString}\" does not exist.")
        }
        // Convert required parameters to usable objects
        for(requiredParam in queryParamsMap[queryString]){
            if(params[requiredParam] == null){
                throw new ParameterException("You must supply parameter \"${requiredParam}\" for query ${queryString}.")
            }
            if(requiredParam == "jurisdiction"){
                params[requiredParam] = parseJurisdiction(params[requiredParam])
            }
            if(requiredParam == "jurisdictions"){
                Set<Jurisdiction> jurisdictions = new HashSet<Jurisdiction>()
                String[] jurisdictionTokens = params[requiredParam].split(",")
                for(jurisdictionToken in jurisdictionTokens){
                    jurisdictions.add(parseJurisdiction(jurisdictionToken))
                }
                if(jurisdictions.isEmpty()){
                    throw new ParameterException("Jurisdiction set cannot be empty.")
                }
                params[requiredParam] = jurisdictions
            }
            if(requiredParam == "point"){
                String pointString = params[requiredParam]
                params[requiredParam] = geometryFactory.createPoint(parseCoordinate(pointString.substring(1, pointString.length() - 1)))
            }
            if(requiredParam == "polygon"){
                String coordinatesString = params[requiredParam]
                String[] coordinateTokens = coordinatesString.substring(2, coordinatesString.length() - 2).split("\\),\\(")
                if(coordinateTokens.length < 4){
                    throw new ParameterException("Polygons must consist of at least four points (the first and last must be the same).")
                }
                Coordinate[] coordinates = new Coordinate[coordinateTokens.length]
                for(i in 0..<coordinates.length){
                    coordinates[i] = parseCoordinate(coordinateTokens[i])
                }
                if(coordinates[0] != coordinates[coordinates.length - 1]){
                    throw new ParameterException("Polygon coordinate sequences must begin and end with the same point.")
                }
                params[requiredParam] = geometryFactory.createPolygon(geometryFactory.createLinearRing(coordinates), null)
            }
            if(requiredParam == "miles"){
                try{
                    params[requiredParam] = Double.parseDouble(params[requiredParam])
                }catch(NumberFormatException nfe){
                    throw new ParameterException("Distance must be a decimal number of miles, not \"${params[requiredParam]}\".")
                }
            }
        }
        // Convert optional params to usable objects
        if(params["type"] != null){
            params["type"] = nameJurisMap.get(params["type"])
            if(params["type"] == null){
                throw new ParameterException("\"${params["type"]}\" is not a valid jurisdiction type (e.g., \"State\", \"ZipCode\", etc.).")
            }
        }
        if(params["parent"] != null){
            params["parent"] = parseJurisdiction(params["parent"])
        }
        if(params["numResults"] != null){
            try{
                params["numResults"] = Integer.parseInt(params["numResults"])
            }catch(NumberFormatException nfe){
                throw new ParameterException("numResults must be an integer, not \"${params["numResults"]}\".")
            }
            if(params["numResults"] < 1){
                throw new ParameterException("numResults must be at least 1, not \"${params["numResults"]}\".")
            }
        }
    }
    private Jurisdiction parseJurisdiction(String jurisId){
        Jurisdiction juris = null
        try{
            juris = Jurisdiction.get(Long.parseLong(jurisId))
        }catch(NumberFormatException nfe){
            throw new ParameterException("Jurisdiction id must be a number of type long, not \"${jurisId}\".")
        }
        if(juris == null){
            throw new ParameterException("No jurisdiction with id ${jurisId} found.")
        }
        return juris
    }
    private Coordinate parseCoordinate(String coordinateString){
        double lon
        double lat
        String[] coordinateTokens = coordinateString.split(",")
        if(coordinateTokens.length != 2){
            throw new ParameterException("Coordinates can only have two coordinates, e.g. \"(A,B)\", not \"(${coordinateString})\".")
        }
        try{
            lon = Double.parseDouble(coordinateTokens[0])
            lat = Double.parseDouble(coordinateTokens[1])
        }catch(NumberFormatException nfe){
            throw new ParameterException("\"Coordinates must consist of two decimal numbers, not \"(${coordinateString})\".")
        }
        return new Coordinate(lon, lat)
    }
    private void filterResultSet(Set<Jurisdiction> resultSet){
        Class type = params["type"]
        Jurisdiction parent = params["parent"]
        if((type == null) && (parent == null)){
            return
        }
        Iterator<Jurisdiction> resultSetIter = resultSet.iterator()
        while(resultSetIter.hasNext()){
            Jurisdiction juris = resultSetIter.next()
            if((type != null) && (juris.class != type)){
                resultSetIter.remove()
                continue
            }
            if((parent != null) && (
                (juris instanceof Country) ||
                ((juris instanceof State) && (juris.country != parent)) ||
                (  ((juris instanceof CongressionalDistrict) ||
                    (juris instanceof SenatorialDistrict) ||
                    (juris instanceof RepresentativeDistrict) ||
                    (juris instanceof Municipality) ||
                    (juris instanceof County) ||
                    (juris instanceof ZipCode)) &&
                (juris.state != parent)) ||
                ((juris instanceof VotingDistrict) && (juris.county != parent))
            )){
                resultSetIter.remove()
                continue
            }
        }
    }
    private void renderResults(Object theResults){
        if(theResults instanceof Set){
            Set<Jurisdiction> resultSet = (Set)theResults
            renderSet(resultSet)
        }
        if(theResults instanceof Jurisdiction){
            Set<Jurisdiction> resultSet = new HashSet<Jurisdiction>()
            resultSet.add((Jurisdiction)theResults)
            renderSet(resultSet)
        }
        if(theResults instanceof Point){
            Point thePoint = (Point)theResults
            render(contentType:"text/xml"){
                results(error:"false"){
                    point(lon:thePoint.getX(), lat:thePoint.getY())
                }
            }
        }
        if(theResults instanceof Envelope){
            Envelope theEnvelope = (Envelope)theResults
            render(contentType:"text/xml"){
                results(error:"false"){
                    envelope(
                        minLon:theEnvelope.getMinX(),
                        maxLon:theEnvelope.getMaxX(),
                        minLat:theEnvelope.getMinY(),
                        maxLat:theEnvelope.getMaxY()
                    )
                }
            }
        }
    }
    private void renderSet(Set<Jurisdiction> resultSet){
        filterResultSet(resultSet)
        Iterator<Jurisdiction> resultSetIterator = resultSet.iterator()
        int numResults = Math.min(params["numResults"], resultSet.size())
        render(contentType:"text/xml"){
            results(error:"false", numResultsShown:numResults, totalResults:resultSet.size()){
                for(int i = 1; i <= numResults; i++){
                    Jurisdiction juris = resultSetIterator.next()
                    Long parentId = null
                    if(juris instanceof State){
                        parentId = juris.country.id
                    }else if(
                        (juris instanceof CongressionalDistrict) ||
                        (juris instanceof SenatorialDistrict) ||
                        (juris instanceof RepresentativeDistrict) ||
                        (juris instanceof Municipality) ||
                        (juris instanceof County) ||
                        (juris instanceof ZipCode)
                    ){
                        parentId = juris.state.id
                    }else if(juris instanceof VotingDistrict){
                        parentId = juris.county.id
                    }
                    jurisdiction(
                        id:juris.id,
                        type:juris.class.getSimpleName(),
                        name:juris.name,
                        longName:juris.longName,
                        parentId:parentId
                    )
                }
            }
        }
    }
    private void renderException(Exception e){
        boolean known = false
        if(e instanceof ParameterException){
            known = true
        }else{
            e.printStackTrace()
        }
        render(contentType:"text/xml"){
            results(error:"true"){
                error(known:known, text:e.getMessage())
            }
        }
    }

    private static Map<String, Class> initializeNameJurisMap(){
        List<Class> jurisClasses = new ArrayList<Class>()
        GeneralUtils.initializeClassList(GeneralUtils.JURISDICTION_PACKAGE, Jurisdiction, jurisClasses)
        Map<String, Class> nameJurisMap = new HashMap<String, Class>()
        for(clazz in jurisClasses){
            nameJurisMap.put(clazz.getSimpleName(), clazz)
        }
        return nameJurisMap
    }
    def query = {
        try{
            println "Request received."
            preprocessParams()
            println "Preprocessing complete."
            Object results = queryMethodMap[params["query"]]()
            println "Query dispatched."
            renderResults(results)
            println "Results rendered."
        }catch(Exception e){
            renderException(e)
            println "Exception rendered."
        }
    }
}
